const BaseService = require('./BaseService');
const tradeDao = require('../dao/TradeDao');
const orderDao = require('../dao/OrderDao');
const userDao = require('../dao/UserDao');
const blockAuditDao = require('../dao/BlockAuditDao');
const ErrorProxy = require('../exception/BusinessExceptionProxy');
const { OrderType, OrderStatus, TradeStatus } = require('../constants/DbEnum');
const LambdaProxy = require('../utils/LambdaProxy');

/**
 * Trade Service
 */
class TradeService extends BaseService {
  /**
   * constructor
   */
  constructor() {
    super(tradeDao);
  }

  /**
   * create trade
   * @param {Trade} trade
   */
  async createTrade(trade) {
    console.info('trade amount', trade.baseCoinAmount);
    console.info('buy id', trade.buyOrderId);
    console.info('sell id', trade.sellOrderId);

    let [buy, sell] = await this._findAndUpdateOrders(trade);

    await this.transaction((t) => {
      let q1 = this.dao.create(trade);
      let q2 = buy.update(buy.get());
      let q3 = sell.update(sell.get());
      return Promise.all([q1, q2, q3]);
    });

    await this._notifySellerConfirmTrade(sell.username);
  }

  /**
   * find and update buy&sell order
   * @param {Trade} trade
   * @return {Promise<[Order, Order]>}
   * @private
   */
  async _findAndUpdateOrders(trade) {
    let [buy, sell] = await Promise.all([
      orderDao.findById(trade.buyOrderId),
      orderDao.findById(trade.sellOrderId),
    ]);

    console.info('found buy & sell', buy, sell);

    if (buy.status == OrderStatus.CANCELED) {
      ErrorProxy.invalidOrderStatus(buy.status);
    }
    if (sell.status == OrderStatus.CANCELED) {
      ErrorProxy.invalidOrderStatus(sell.status);
    }

    let newBuyAmount = buy.get('currentBaseCoinAmount') - trade.baseCoinAmount;
    let newSellAmount = sell.get('currentBaseCoinAmount') - trade.baseCoinAmount;

    console.info('calculate new amount', newBuyAmount, newSellAmount);

    buy.set('currentBaseCoinAmount', newBuyAmount);
    sell.set('currentBaseCoinAmount', newSellAmount);

    if (newBuyAmount < 0) {
      ErrorProxy.commonBusinessException(400, 'buy order current amount insufficient');
    } else if (newBuyAmount == 0) {
      buy.set('status', OrderStatus.COMPLETED);
    }

    if (newSellAmount < 0) {
      ErrorProxy.commonBusinessException(400, 'sell order current amount insufficient');
    } else if (newSellAmount == 0) {
      sell.set('status', OrderStatus.COMPLETED);
    }

    console.info('to be update buy & sell', buy, sell);

    return [buy, sell];
  }

  /**
   * commit trade
   * @param {OrderType} orderType
   * @param {Integer} tradeId
   * @param {String} username
   * @param {String} txId
   * @return {Promise<*>}
   */
  async commit({ orderType, tradeId, username, txId }) {
    let newValues = { status: TradeStatus.PENDING };
    let where = { id: tradeId, status: TradeStatus.INIT };

    if (orderType == OrderType.limitedBuy) {
      newValues.counterCoinTx = txId;
      where.buyUsername = username;
    } else if (orderType == OrderType.limitedSell) {
      newValues.baseCoinTx = txId;
      where.sellUsername = username;
    }

    return this.dao.model.update(newValues, { where, validate: false });
  }

  /**
   * complete trade
   * @param {Integer} id
   * @return {Promise}
   */
  async complete(id) {
    let trade = await this.dao.findById(id);
    trade.set('status', TradeStatus.COMPLETED);

    let [buyer, seller, buyOrder, sellOrder] = await Promise.all([
      userDao.findOne({ username: trade.buyUsername }),
      userDao.findOne({ username: trade.sellUsername }),
      orderDao.findOneWithTrades(trade.buyUsername, trade.buyOrderId, [
        TradeStatus.INIT,
        TradeStatus.PENDING,
      ]),
      orderDao.findOneWithTrades(trade.sellUsername, trade.sellOrderId, [
        TradeStatus.INIT,
        TradeStatus.PENDING,
      ]),
    ]);

    // return deposit to buyer and seller
    let sellIncrementBalance = (trade.price + trade.sellDepositPrice) * trade.baseCoinAmount;
    let buyIncrementBalance = (buyOrder.price - trade.price) * trade.baseCoinAmount;

    this._updateOrderStatus(buyOrder, id);
    this._updateOrderStatus(sellOrder, id);

    return this.transaction((t) => {
      const promises = new Array();
      promises.push(trade.update(trade.get()));
      if (sellIncrementBalance > 0) {
        promises.push(seller.increment({ balance: sellIncrementBalance }));
      }
      if (buyIncrementBalance > 0) {
        promises.push(buyer.increment({ balance: buyIncrementBalance }));
      }
      promises.push(buyOrder.update(buyOrder.get()));
      promises.push(sellOrder.update(sellOrder.get()));

      return Promise.all(promises);
    });
  }

  /**
   * expire trade
   * @param {Integer} id
   */
  async expire(id) {
    let trade = await this.dao.findById(id);
    if (await this._checkIfExpired(trade)) {
      trade.set('status', TradeStatus.EXPIRED);

      let [buyer, seller, buyOrder, sellOrder] = await Promise.all([
        userDao.findOne({ username: trade.buyUsername }),
        userDao.findOne({ username: trade.sellUsername }),
        orderDao.findOneWithTrades(trade.buyUsername, trade.buyOrderId, [
          TradeStatus.INIT,
          TradeStatus.PENDING,
        ]),
        orderDao.findOneWithTrades(trade.sellUsername, trade.sellOrderId, [
          TradeStatus.INIT,
          TradeStatus.PENDING,
        ]),
      ]);

      // sent seller's deposit to buyer
      const sellDeposit = trade.sellDepositPrice * trade.baseCoinAmount;

      // rollback buy order's currentBaseCoinAmount
      const inStatus = [OrderStatus.PENDING, OrderStatus.COMPLETED];
      if (inStatus.includes(buyOrder.status)) {
        const amount = buyOrder.currentBaseCoinAmount + trade.baseCoinAmount;
        buyOrder.set('currentBaseCoinAmount', amount);
        buyOrder.set('status', OrderStatus.PENDING);
      }

      this._updateOrderStatus(buyOrder, id);
      this._updateOrderStatus(sellOrder, id);

      const blockAudit = this._buildBlockAudit(trade);

      await this.transaction((t) => {
        const promises = new Array();
        promises.push(trade.update(trade.get()));
        promises.push(buyer.increment({ balance: sellDeposit }));
        promises.push(buyOrder.update(buyOrder.get()));
        promises.push(sellOrder.update(sellOrder.get()));
        promises.push(blockAudit.save());

        return Promise.all(promises);
      });

      await this._notifyTradeExpired(buyer.phone, seller.phone);
    } else {
      ErrorProxy.commonBusinessException(400, 'this trade has not yet expired');
    }
  }

  /**
   * check trade if expired
   * @param {Trade} trade
   * @return {Boolean}
   * @private
   */
  async _checkIfExpired(trade) {
    let isExpired = false;

    if (trade.status == TradeStatus.INIT && Date.now() >= trade.expiredTimestamp) {
      const result = await LambdaProxy.fetchBlockchainTxHash(trade.id, trade.baseCoinType);

      console.info('fetch block chain tx hash', result);

      if (result.txHash) {
        trade.status = TradeStatus.PENDING;
        trade.baseCoinTx = result.txHash;
        await trade.update(trade.get());
      } else {
        isExpired = true;
      }
    }

    return isExpired;
  }

  /**
   * Update Order Status
   * @param {Order} order
   * @param {Integer} ignoreTradeId
   * @private
   */
  _updateOrderStatus(order, ignoreTradeId) {
    order.historicalTradeCount++;
    if (order.status == OrderStatus.COMPLETED) {
      let trades = order.Trades;
      if (trades.length == 0 || (trades.length == 1 && trades[0].id == ignoreTradeId)) {
        order.set('status', OrderStatus.HISTORICAL);
      }
    }
    console.info('to be update order', order);
  }

  /**
   * notify seller confirm trade
   * @param {String} username
   * @private
   */
  async _notifySellerConfirmTrade(username) {
    try {
      const seller = await userDao.findOne({ username });
      await LambdaProxy.sendSMS(seller.phone, '【宽客】您有订单需要确认，请及时处理');
    } catch (e) {
      console.warn('send sms failed', e);
    }
  }

  /**
   * _notifyTradeExpired
   * @param {String} buyerPhone
   * @param {String} sellerPhone
   * @private
   */
  async _notifyTradeExpired(buyerPhone, sellerPhone) {
    try {
      await Promise.all([
        LambdaProxy.sendSMS(
          buyerPhone,
          '【宽客】您的买单因为对方确认超时，并收到违约补偿，请在成交明细查看详情'
        ),
        LambdaProxy.sendSMS(
          sellerPhone,
          '【宽客】您的卖单因为确认超时被扣除保证金，请在成交明细查看详情'
        ),
      ]);
    } catch (e) {
      console.warn('send sms failed', e);
    }
  }

  /**
   * build block audit
   * @param {Trade} trade
   * @return {BlockAudit}
   * @private
   */
  _buildBlockAudit(trade) {
    return blockAuditDao.model.build({
      username: trade.sellUsername,
      orderId: trade.sellOrderId,
      tradeId: trade.id,
    });
  }
}

module.exports = new TradeService();
